from typing import Optional, Union, Iterable

import pyrr.geometry


class Camera:
    def __init__(
            self, aspect: float, fov: float = 60, near: float = 1,
            far: float = 100, position: tuple[float, float, float] = (0, 0, 1),
            focus: tuple[float, float, float] = (0, 0, 0),
    ):
        self._aspect = aspect
        self._fov = fov
        self._near = near
        self._far = far
        self._position: pyrr.Vector3 = pyrr.Vector3(position, dtype='f4')
        self._focus: pyrr.Vector3 = pyrr.Vector3(focus, dtype='f4')
        self._up = pyrr.Vector3((0, 1, 0), dtype='f4')
        self._project_matrix: Optional[pyrr.Matrix44] = None
        self._camera_matrix: Optional[pyrr.Matrix44] = None
        self._matrix: Optional[pyrr.Matrix44] = None

    @property
    def fov(self) -> float:
        return self.fov

    @fov.setter
    def fov(self, fov: float):
        self._project_matrix = None
        self._matrix = None
        self._fov = fov

    @property
    def aspect(self) -> float:
        return self.aspect

    @aspect.setter
    def aspect(self, aspect: float):
        self._project_matrix = None
        self._matrix = None
        self._aspect = aspect

    @property
    def near(self) -> float:
        return self._near

    @near.setter
    def near(self, near: float):
        self._project_matrix = None
        self._matrix = None
        self._near = near

    @property
    def far(self) -> float:
        return self._far

    @far.setter
    def far(self, far: float):
        self._project_matrix = None
        self._matrix = None
        self._far = far

    @property
    def focus(self) -> pyrr.Vector3:
        return self._focus.xyz

    def set_position(
            self, x: Union[float, Iterable] = None,
            y: float = None, z: float = None
    ):
        if isinstance(x, Iterable):
            new_pos = pyrr.Vector3(x)
        else:
            if x is None:
                x = self._position.x
            if y is None:
                y = self._position.y
            if z is None:
                z = self._position.z
            new_pos = pyrr.Vector3((x, y, z))
        delta = new_pos - self._position
        self._position = new_pos
        self._focus += delta
        self._camera_matrix = None
        self._matrix = None

    def get_position(self) -> pyrr.Vector3:
        return self._position.xyz

    def lookat(
            self, x: Union[float, Iterable] = None,
            y: float = None, z: float = None
    ):
        if isinstance(x, Iterable):
            self._focus.xyz = x
        else:
            if x is not None:
                self._focus.x = x
            if y is not None:
                self._focus.y = y
            if z is not None:
                self._focus.z = z
        self._camera_matrix = None
        self._matrix = None

    def _update_project_matrix(self):
        aspect = self._aspect
        fov = self._fov
        near = self._near
        far = self._far
        self._project_matrix = pyrr.Matrix44.perspective_projection(fov, aspect, near, far)

    def _update_camera_matrix(self):
        up = self._up.normalized
        direction = (self._position - self._focus).normalized
        right = (up ^ direction).normalized
        up = (direction ^ right).normalized
        translate_matrix = pyrr.Matrix44.from_translation(-self._position, dtype='f4')
        lookat_matrix = pyrr.Matrix44([
            [right.x, up.x, direction.x, 0],
            [right.y, up.y, direction.y, 0],
            [right.z, up.z, direction.z, 0],
            [0, 0, 0, 1],
        ], dtype='f4')
        self._camera_matrix = lookat_matrix * translate_matrix

    def _update_matrix(self):
        self._matrix = self._project_matrix * self._camera_matrix

    @property
    def matrix(self) -> pyrr.Matrix44:
        if self._project_matrix is None:
            self._update_project_matrix()
        if self._camera_matrix is None:
            self._update_camera_matrix()
        if self._matrix is None:
            self._update_matrix()
        return self._matrix
